package org.xx.armory.commons;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * 打印对象内容的工具类
 *
 * @author Haart
 */
public final class ObjectDumper {
    /**
     * 禁止构造该类。
     */
    private ObjectDumper() {
        // Prevent to construct a instance.
        assert false;
    }

    /**
     * 将对象打印输出为字符串形式。
     *
     * <p>
     * 示例:
     * </p>
     *
     * <pre>
     * dump(null) == "null";
     * dump(1234.56) == "1234.56";
     * int[] a1 = new int[] { 3, 4, 5 };
     * dump(a1) == "[3, 4, 5]";
     * </pre>
     *
     * @param obj
     *         待输出的对象
     * @return 对应的字符串形式。
     */
    public static String dump(Object obj) {
        DumpContext ctx = new DumpContext();

        return dump(ctx, obj);
    }

    /**
     * 将对象打印输出为字符串形式。
     *
     * @param ctx
     *         输出上下文
     * @param obj
     *         待输出的对象
     * @return 对应的字符串形式。
     */
    private static String dump(
            DumpContext ctx,
            Object obj
    ) {
        // Null.
        if (obj == null) {
            return "null";
        }

        Class<?> clazz = obj.getClass();

        // Primitive.
        if (clazz.isPrimitive()) {
            return String.valueOf(obj);
        }

        if (clazz == Integer.class || clazz == Float.class || clazz == Double.class
                || clazz == Byte.class || clazz == Long.class || clazz == BigDecimal.class
                || clazz == Short.class || clazz == Boolean.class || clazz == Void.class) {
            return String.valueOf(obj);
        }

        if (clazz == Character.class) {
            return "'" + obj + "'";
        }

        if (CharSequence.class.isAssignableFrom(clazz)) {
            return "\"" + obj + "\"";
        }

        if (Date.class.isAssignableFrom(clazz)) {
            return "Date\"" + Date.class.cast(obj) + "\"";
        }

        if (Calendar.class.isAssignableFrom(clazz)) {
            return "Calendar\"" + (Calendar.class.cast(obj)).getTime() + "\"";
        }

        if (clazz.getName().contains("Log")) {
            return clazz.getName();
        }

        if (ctx.contains(obj)) {
            return "...";
        }

        ctx.add(obj);

        // Array.
        if (clazz.isArray()) {
            final Class<?> componentType = clazz.getComponentType();
            final StringBuilder sb = new StringBuilder();
            final int al = Array.getLength(obj);
            sb.append(getTypeName(componentType));
            sb.append("(size=").append(al).append(")[");
            for (int i = 0; i < al; ++i) {
                if (i > 0) {
                    sb.append(",");
                }
                if (componentType.isPrimitive()) {
                    sb.append(Array.get(obj, i));
                } else {
                    sb.append(dump(ctx, Array.get(obj, i)));
                }
            }
            sb.append("]");
            return sb.toString();
        }

        // Collection
        if (Collection.class.isAssignableFrom(clazz)) {
            final Collection<?> coll = Collection.class.cast(obj);
            final StringBuilder sb = new StringBuilder();
            final int al = coll.size();
            sb.append("(size=").append(al).append(")[");
            boolean first = true;
            for (final Object item : coll) {
                if (!first) {
                    sb.append(",");
                }
                sb.append(dump(ctx, item));
                first = false;
            }
            sb.append("]");
            return sb.toString();
        }

        // Map.
        if (Map.class.isAssignableFrom(clazz)) {
            StringBuilder sb = new StringBuilder();
            Map<?, ?> map = (Map<?, ?>) obj;
            sb.append(getTypeName(clazz));

            sb.append("{");
            boolean ff = true;
            for (Map.Entry<?, ?> entry : map.entrySet()) {
                if (!ff) {
                    sb.append(",");
                }

                sb.append(entry.getKey());
                sb.append(":");
                try {
                    sb.append(dump(ctx, entry.getValue()));
                } catch (Exception e) {
                    sb.append("<").append(e).append(">");
                }

                ff = false;
            }

            sb.append("}");

            return sb.toString();
        }

        // Enum
        if (clazz.isEnum()) {
            return obj.toString();
        }

        // Object.
        StringBuilder sb = new StringBuilder();
        sb.append(getTypeName(clazz));
        do {
            sb.append("{");
            Field[] fields = clazz.getDeclaredFields();
            AccessibleObject.setAccessible(fields, true);

            boolean ff = true;
            for (final Field field : fields) {
                int modifiers = field.getModifiers();
                if (Modifier.isStatic(modifiers)) {
                    continue;
                }

                if (!ff) {
                    sb.append(",");
                }

                if (Modifier.isPublic(modifiers)) {
                    sb.append("+");
                }
                sb.append(field.getName());
                sb.append(":");
                try {
                    sb.append(dump(ctx, field.get(obj)));
                } catch (Exception e) {
                    sb.append("<").append(e).append(">");
                }

                ff = false;
            }

            sb.append("}");
            clazz = clazz.getSuperclass();
        } while (clazz != null && clazz != Object.class);

        return sb.toString();
    }

    /**
     * 获取指定类型的名字。
     *
     * @param clazz
     *         指定的类型。
     * @return 指定类型的名字。
     * @throws IllegalArgumentException
     *         如果参数{@code clazz}是{@code null}。
     */
    private static String getTypeName(Class<?> clazz) {
        assert (clazz != null);

        if (clazz == String.class) {
            return "String";
        }

        if (clazz == BigDecimal.class) {
            return "decimal";
        }

        String className = clazz.getSimpleName();
        if (className.isEmpty()) {
            String name = clazz.getName();
            int ld = name.lastIndexOf('.');
            int ls = name.lastIndexOf('$');
            if (ld == -1 && ls == -1) {
                return name;
            } else {
                int ll = ld > ls ? ld : ls;
                return name.substring(ll);
            }
        } else {
            return className;
        }
    }

    /**
     * 输出上下文, 用来记录当前级别
     *
     * @author Haart
     */
    private static final class DumpContext {
        private final Set<Object> visited;

        public DumpContext() {
            visited = new HashSet<>();
        }

        /**
         * 将指定的对象加入输出上下文，表示此对象正在输出。
         *
         * @param obj
         *         正在输出的对象，如果是{@code null}则不添加。
         */
        public void add(Object obj) {
            if (obj != null) {
                visited.add(obj);
            }
        }

        /**
         * 判断指定的对象是否位于输出上下文中。
         *
         * @param obj
         *         指定的对象。
         * @return 如果正在输出则返回{@code true}，否则返回{@code false}。
         */
        public boolean contains(Object obj) {
            return obj != null && visited.contains(obj);
        }
    }
}
